/*file chiron src/sound.js */
/*preamble

    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    The license terms are stated in full in <license.rst> and at the end
    of all source files.

*/

include('base.js');
include('http.js');
include('event.js');
include('time.js');
include('optioned.js');
var flash = require('flash.js');
var environment = require('environment.js');

var bridge; /* the flash movie object */
var volume = 1.0; /* a volume multiplier from 0 to 1 */
var mute = 1.0; /* a volume multiplier from 0 to 1 */
var sounds = {}; /* urls to Sound objects */
var plays = {}; /* urls to Play objects */
var playing = true; /* a boolean multiplier for whether any sound plays */

/*** ready
    a ``State`` ``Event`` that indicates that the module is ready
    to load and play sounds.
*/
exports.ready = State();

var assertReady = function () {
    if (!ready.bool())
        throw Error("sound.js not ready.  observe the module's ready event.");
};

/*** play
    plays a sound on a given URL.
    returns a ``Play`` object representing the playback of the sound
    if the ``ready`` signal has been sent and no callback has been
    provided.

    accepts:
     - ``url`` or ``options``, an optional dictionary or dictionary-like object
       containing configuration options for the ``Play`` instance.
       any play options that ``Play`` cannot accept are passed down
       to the underlying ``Sound`` object.
     - ``finished``, an optional function to call when the playback finishes.

    options:
     - ``url``, the URL of the sound relative to the environment URL.
     - ``volume``, the initial volume to play the sound at, from 0 to 1
     - ``pan``, the initial left-to-right pan to play at, from 0 to 1
     - ``transform``, a dict-like object like ``{ll: ll, lr: lr, rl: rl, rr: rr}`` 
     - ``position``, the initail position to play the sound at in miliseconds
     - ``progress``, the initial percent progress to play the sound at, from 0 to 1
     - ``finished``, a callback for when the playback finishes
     - ``autoPlay``, instructs the playback to assume that the corresponding
       ``Sound`` object was called with ``autoPlay`` as well so that it
       doesn't restart the sound when the file finishes loading.

*/
exports.play = function (options, onFinished) {
    if (isInstance(options, String)) options = {'url': options};
    options = dict(options).added({'autoPlay': true});
    var soundOptions = {
        'url': options.get('url'),
        'autoPlay': true
    };

    if (onFinished || !ready.bool()) {
        return ready.observe(function () {
            sound(soundOptions).play(options, onFinished);
        });
    } else {
        assertReady();
        return sound(soundOptions).play(options);
    }
};

/*** sound
    get a new or cached ``Sound`` object for a given URL.

    accepts:
     - ``url`` or ``options``, a dict-like object of options.  Some options may
       be ignored if the ``Sound`` object has already been created
       and cached.
     - ``loaded`` an optional function to call when the sound
       finishes loading.


    sound options:
     - ``url``, required URL of the sound file fully qualified or relative
       to the environment (the page loading the sound).
     - ``ready``, a callback for when the sound is ready to play
     - ``id3``, a callback for when id3 information is available
     - ``volume``, a volume multiplier for all playbacks of this sound
     - ``streaming``, whether to play the sound before it finishes
       loading.  Streaming causes Flash to play the sound as soon as
       the remaining bytes can be downloaded before the sound finishes
       playing.  ``true`` by default.
     - ``autoPlay``, causes the Sound to begin playing immediately.
       This option should be used in conjunction with the same option
       on ``Sound.play`` to immediately construct a ``Play`` object
       bound to the automatically played ``Sound``.

*/
exports.sound = function (options, readyObserver) {
    if (isInstance(options, String)) options = {'url': options};
    options = dict(options);
    if (readyObserver) options.set('ready', readyObserver);
    var url = options.get('url');

    /*todo*/
    assertReady();
    var sound = sounds[url];
    if (no(sounds[url])) {
        sound = Sound(options);
    } else {
        sound.setOptions(options);
    }
    return sound;
};

/*** getVolume
    returns the site volume.  This is a multiplier on all sounds
    and playbacks.
*/
exports.getVolume = function () {
    return volume;
};

/*** setVolume
    sets the site volume.  This is a multiplier on all sounds and playbacks.
*/
exports.setVolume = function (volumeValue) {
    if (no(volumeValue)) volumeValue = 1.0;
    volume = volumeValue;
    items(plays).eachArgs(function (key, value) {
        value.setVolume();
    });
};

/*** getMute
    returns whether the site volume has been muted.
*/
exports.getMute = function () {
    return mute == 0;
};

/*** setMute
    sets or resets the site to or from a mute state.
*/
exports.setMute = function (muteValue) {
    if (no(muteValue)) muteValue = true;
    mute = muteValue ? 0 : 1;
    items(plays).eachArgs(function (key, value) {
        value.setVolume();
    });
};

/*** toggleMute */
exports.toggleMute = function () {
    mute = 1.0 - mute;
    items(plays).eachArgs(function (key, value) {
        value.setVolume();
    });
};

/*** pause
    pauses all playbacks.
*/
exports.pause = function () {
    setPlaying(false);
};

/*** resume
    resumes all playbacks.
*/
exports.resume = function () {
    setPlaying(true);
};

/*** toggle
    toggles whether all playbacks are playing.
*/
exports.toggle = function () {
    setPlaying(!getPlaying());
};

/*** setPlaying
    sets whether all playbacks are playing.
*/
exports.setPlaying = function (value) {
    if (no(value)) value = true;
    playing = value;
    items(plays).eachArgs(function (key, value) {
        value.setPlaying();
    });
};

/*** getPlaying
    returns whether playbacks are suspended.
*/
exports.getPlaying = function () {
    return playing;
};

/*** Play
    creates a playback control object and begins playing
    a sound.

    accepts:
     - ``url`` or ``options`` (see ``play``)

    signals:
     - ``finished`` state

*/
exports.Play = type([Optioned, Observable], function (self, supr) {

    var self = this;
    var url;
    var playing = false;
    var position = 0;
    var volume = 1.0;
    var autoPlay = false;

    self.init = function (options, finishedObserver) {
        if (isInstance(options, String)) options = {'url': options};

        supr.init(options);

        if (plays[url]) {
            plays[url].signal('finished');
        }
        plays[url] = self;

        /* ...and, pass the continuation */
        if (finishedObserver) {
            self.observe('finished', function () {
                finishedObserver(self);
            });
        }

        if (!autoPlay) {
            sounds[url].observe('ready', function () {
                self.resume();
            });
        } else {
            playing = true;
            if (sounds[url].ready.bool()) {
                self.resume();
            }
        }
    };

    /**** finished */
    self.setSignal('finished', State(Event, function () {
        position = 1;
        playing = false;
    }));

    /**** setOption */
    self.setOption = function (key, value) {
        if (key == 'url') {
            url = value;
        } else if (key == 'volume') {
            self.setVolume(value);
        } else if (key == 'finished') {
            self.observe('finished', value);
        } else if (key == 'autoPlay') {
            autoPlay = value;
        } else if (key == 'position') {
            self.setPosition(value);
        } else if (key == 'progress') {
            self.setProgress(value);
        } else {
            self.getSound().setOption(key, value);
        }
    };

    /**** getUrl */
    self.getUrl = function () {
        return url;
    };

    /**** isPlaying */
    self.isPlaying = function () {
        return playing;
    };

    /**** getSound
        returns the underlying ``Sound`` object for this
        playback.
    */
    self.getSound = function () {
        return sounds[url];
    };

    /**** getVolume */
    self.getVolume = function () {
        return volume;
    };

    /**** setVolume */
    self.setVolume = function (value) {
        assertReady();
        if (this != plays[url]) return;
        if (value) {
            volume = value;
        }
        var v = floor(
            volume * /* play */
            100 /* flash volume units */
        )
        bridge.setVolume(
            url,
            v
        );
    };

    /**** setPan */
    self.setPan = function (pan) {
        assertReady();
        bridge.setPan(url, pan);
    };

    /**** setTransform */
    self.setTransform = function (ll, lr, rl, rr) {
        assertReady();
        if (isInstance(ll, Number)) {
            bridge.setTransform(
                url,
                {
                    'll': ll,
                    'lr': lr,
                    'rl': rl,
                    'rr': rr
                }
            );
        } else {
            bridge.setTransform(url, object(ll));
        }
    };

    /**** setPosition */
    self.setPosition = function (position) {
        if (self != plays[url]) return;
        self.setPlaying(playing, position);
    };


    /**** getPosition */
    self.getPosition = function () {
        assertReady();
        if (playing) {
            return bridge.getPosition(url);
        } else {
            return position;
        }
    };

    /**** getDuration */
    /*todo memoize*/
    self.getDuration = function () {
        assertReady();
        return bridge.getDuration(url);
    };

    /**** getProgress */
    self.getProgress = function () {
        assertReady();
        if (playing) {
            return bridge.getProgress(url);
        } else {
            return bridge.getProgress(url, position);
        }
    };

    /**** setProgress */
    self.setProgress = function (progressValue) {
        self.setPosition(self.getDuration() * progressValue);
    };

    /**** start */
    self.start = function () {
        self.setPlaying(true, 0);
    };

    /**** stop */
    self.stop = function () {
        self.setPlaying(false, 0); 
    };

    /**** resume */
    self.resume = function () {
        self.setPlaying(true, position);
    };

    /**** pause */
    self.pause = function () {
        self.setPlaying(false);
    };

    /**** toggle */
    self.toggle = function () {
        if (playing) {
            self.pause();
        } else {
            self.resume();
        }
    };

    /**** setPlaying */
    self.setPlaying = function (playingValue, positionValue, loops) {
        assertReady();

        if (no(positionValue)) {
            position = bridge.getPosition(url);
        } else if (playing) {
            position = positionValue;
        }

        if (!no(playingValue)) {
            playing = playingValue;
        }

        if (no(loops)) loops = 1;

        if (getPlaying() && playing) { 
            bridge.start(url, position, loops);
        } else {
            bridge.stop(url);
        }
    };

    /**** getPlaying */
    self.getPlaying = function () {
        return playing && plays[url] == this;
    };

});

/*** Sound

    accepts:
     - ``options`` or ``url`` (see ``sound``)

    signals:
     - ``ready`` state meaning that the file is fully loaded.
     - ``id3`` a state meaning that the id3 information is
       available.

*/
exports.Sound = type([Optioned, Observable], function (self, supr) {

    var self = this;
    var url;
    var streaming = false;
    var volume = 1.0;
    var autoPlay = false;
    var progress = 0;

    self.init = function (options, readyObserver) {
        assertReady();

        if (isInstance(options, String)) options = {'url': options};

        supr.init(options);

        if (sounds[url]) {
            throw Error(
                "use sound to get Sound objects.  " +
                "A Sound object has already been " +
                "created for " + repr(url)
            );
        }

        /* ...and, pass the continuation */
        if (readyObserver) {
            self.observe('ready', function () {
                readyObserver(self);
            });
        }

        sounds[url] = this;
        bridge.load(url, streaming, autoPlay);
    };

    /**** ready */
    self.setSignal('ready', State());

    /**** id3 */
    self.setSignal('id3', State());

    /**** setOption */
    self.setOption = function (key, value) {
        if (key == 'url') {
            url = value;
        } else if (key == 'ready') {
            self.observe('ready', value); 
        } else if (key == 'id3') {
            self.observe('id3', value);
        } else if (key == 'volume') {
            self.setVolume(value);
        } else if (key == 'streaming') {
            streaming = value;
        } else if (key == 'autoPlay') {
            autoPlay = value;
        } else {
            throw Error(repr(key) + ' is not a sound option');
        }
    };

    /**** getUrl */
    self.getUrl = function () {
        return url;
    };

    /**** getStreaming */
    self.getStreaming = function () {
        return streaming;
    };

    /**** getDuration */
    self.getDuration = function () {
        assertReady();
        return bridge.getDuration(url);
    };

    /**** getBytesLoaded */
    self.getBytesLoaded = function () {
        assertReady();
        return bridge.getBytesLoaded(url);
    };

    /**** getBytesTotal */
    self.getBytesTotal = function () {
        assertReady();
        return bridge.getBytesTotal(url);
    };

    /**** getProgress */
    self.getProgress = function () {
        if (progress < 1) {
            progress = self.getBytesLoaded() / self.getBytesTotal();
        }
        return progress;
    };

    /**** getVolume */
    self.getVolume = function () {
        return volume;
    };

    /**** setVolume */
    self.setVolume = function (volumeValue) {
        volume = volumeValue;
        if (plays[url]) {
            plays[url].setVolume();
        }
    };

    /**** getId3 */
    self.getId3 = function () {
        assertReady();
        return bridge.getId3(url);
    };

    /**** play */
    self.play = function (options, finishedObserver) {
        return Play(add(options, {'url': url}), finishedObserver);
    };

    /**** del */
    self.del = function () {
        assertReady();
        bridge.unload(url);
        sounds[url] = undefined;
    };

});

/* create and bridge a sound manager Flash movie */
flash.bridge(resolve('sound.swf'), function (sender, receiver) {

    log('sound.js: flash ready');

    receiver.log = function (message) {
        log('sound.js: ' + message);
    };

    receiver.id3 = function (url, id3) {
        log('sound.js: ' + repr(url) + ': id3 information ' + repr(id3));
        sounds[url].id3(id3);
    };
    receiver.ready = function (url) {
        log('sound.js: ' + repr(url) + ': ready');
        sounds[url].ready();
    };
    receiver.finished = function (url) {
        log('sound.js: ' + repr(url) + ': finished');
        plays[url].finished();
    };

    bridge = sender;
    ready();

    log('sound.js: ready');
});

/**
    References
    ----------

    http://www.schillmania.com/projects/soundmanager2/
    Scott Shiller's Sound Manager 2.  This is the origin of this
    module, although none of the original code remains.

    http://www.brajeshwar.com/reference/as2/Sound.html
    A copy of the Action Script sound API documentation

*/


/*license

    Legal
    =======
    
    Chiron is a component of the Tale web-game project.
    
    See <credit.txt> for a complete list of
    contributions and their licenses.  All contributions are provided
    under permissive, non-viral licenses including MIT, BSD, Creative Commons
    Attribution 2.5, Public Domain, or Unrestricted.
    
    
    License
    =======
    
    Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
    MIT License
    
    
    MIT License
    -----------
    
    Permission is hereby granted, free of charge, to any person
    obtaining a copy of this software and associated documentation
    files (the "Software"), to deal in the Software without
    restriction, including without limitation the rights to use,
    copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following
    conditions:
    
    The above copyright notice and this permission notice shall be
    included in all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    OTHER DEALINGS IN THE SOFTWARE.

*/

